Repository navigation
HermitCrab optimization round record: what was tried, and a faster package that is not mergeable - #490
HermitCrab optimization round record: what was tried, and a faster package that is not mergeable#490johnml1135 wants to merge 34 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #490 +/- ##
=======================================
Coverage 74.57% 74.57%
=======================================
Files 453 453
Lines 38314 38314
Branches 5280 5280
=======================================
Hits 28573 28573
Misses 8576 8576
Partials 1165 1165 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
004e199 to
3252bbc
Compare
|
Two further optimization ideas were investigated against this ledger, on a branch based on Neither appears in the existing 22 rows. Row 5 covers deterministic synthesis fold sharing and the rejected two-pass experiment covers nondeterministic traversal; neither is the idea below. Row 6 is the synthesis-side sibling of the second idea. Idea A — a
|
| grammar | words | rule attempts off → on | Σ det arc checks off → on | suppressed | Σ nondet arc checks | det share of FST work |
|---|---|---|---|---|---|---|
| Indonesian | 121 | 15,040 → 15,040 | 34,386 → 34,386 | 0 | 990,111 | 3.4 % |
| Mbugwe | 37 | 4,351,113 → 4,351,113 | 760,833 → 760,829 | 4 | 21,334,695 | 3.4 % |
| Sena | 35 | 22,416,707 → 22,416,707 | 194,014 → 192,515 | 192 | 124,891,129 | 0.16 % |
| Amharic | 37 | 71,315 → 71,315 | 9,979 → 9,979 | 0 | 44,559,287 | 0.02 % |
| Aweti | 22 | 24,274,820 → 24,274,820 | 192,827 → 192,779 | 8 | 97,502,953 | 0.20 % |
Parse-signature multisets identical on every completed word. The reason it is a no-op is structural: the deterministic traversal only runs on the synthesis side (analysis affix matchers set AllSubmatches, analysis rewrite/metathesis set Nondeterministic), and synthesis shapes carry almost no optional annotations, so the skip-vs-consume fork it would collapse rarely occurs. Deterministic traversal is 0.02–3.4 % of all CheckInputMatch calls, and dedup removes ≤ 0.8 % of that.
Where the duplicate explosion the idea was aiming at actually lives is the nondeterministic set, which already exists: on Amharic it suppressed 30.1 M candidate instances against 15.2 M pushed (66 % duplicates), and on one heavy word 82.7 M against 38.5 M.
Two latent issues noticed while auditing the existing keys, neither reachable from HermitCrab today:
NondeterministicFstTraversalMethod's key carries the output action list but not which annotations the actions applied to. Two paths through optional annotations can share state, index, registers and action sequence while having modified different output annotations. Unreachable because no HermitCrab FST carriesIFstOperations.- Both nondeterministic keys omit
VariableBindings, whichCheckInputMatchreads. Whether two equal(State, AnnotationIndex, Registers)keys can carry different bindings on a real pattern is untested. - Both store the mutable
Register[,]directly in the key whileExecuteCommandslater mutates it. This can only cause a missed dedup, never a wrong one.
Idea B — canonicalize the order of independent rules in an Unordered analysis stratum
The analysis-side counterpart of row 6. In an Unordered stratum the cascade explores both [A,B] and [B,A]; if A and B commute, one order is redundant work. The prune skips a dispatch when the candidate is orthogonal to the trail's last rule and precedes it in source order — adjacent-transposition normalization, with non-orthogonal rules and templates acting as barriers.
Orthogonality was implemented as three cumulative levels — L0 shape (pure prefix vs pure suffix, single application, edge-blind stem pattern, no ModifyFromInput/variables), L1 + syntactic-feature commutation decided by enumerating the finite feature lattice the two rules mention in both the analysis (IsUnifiable gate then Add/Clear) and synthesis (Unify then PriorityUnion) directions, L2 + MPR/stem-name/partial/blocking. A second, independently written census over the XML agreed with the object-model implementation on every unordered stratum: 176 L0 pairs, 60 L1, 36 L2 across the five grammars.
Measured: 0.00–0.05 % of rule-unapplication attempts, parity clean on every word.
| grammar | words | attempts off → on | reduction | dispatches | skipped |
|---|---|---|---|---|---|
| Indonesian | 121 | 15,040 → 15,040 | 0.00 % | 21,030 | 0 |
| Mbugwe | 37 | 4,351,113 → 4,351,113 | 0.00 % | 1,631,322 | 858 |
| Sena | 35 | 22,416,707 → 22,404,555 | 0.05 % | 6,603,760 | 12,104 |
| Amharic | 29 | 36,774 → 36,758 | 0.04 % | 27,264 | 16 |
| Aweti | 12 | 3,014,439 → 3,014,439 | 0.00 % | 1,213,430 | 0 |
| Sena at L0 (unsound control, same 35 words) | 35 | 25,823,709 → 24,179,995 | 6.37 % | 7,442,610 | 67,090 |
The L0 control is what makes this a finding rather than a null result: widening Sena from 2 qualifying pairs to 52 moves the reduction from 0.05 % to 6.37 %, so the limit is the predicate, not the hook. But each narrowing step is load-bearing — of Sena's 52 L0 pairs, 30 fail L1 because their syntactic features genuinely do not commute, and 20 fail L2 on the partial-rule clause, which is necessary there because that stratum holds 24 affix templates so IsLastAppliedRuleFinal takes a value and both final-template gates in SynthesisAffixProcessRule can fire. 6.37 % is what an incorrect prune wins.
A trail-depth census explains why widening cannot reach further. Instrumenting every dispatch with the length of the trail it extends:
- Mbugwe, 155,672 dispatches: 68 % sit on trails 9+ rules deep, but at only 0.8 % does the trail's last rule have any orthogonal partner even at L0.
- A heavy Sena word, 76,777 dispatches: 67 % at depth 9+, 16.6 % partner-available, 2.3 % actually skipped → the 6.37 %.
So the permutation blow-up is real and deep, but the rules filling those trails are ones the criterion excludes on shape (Mbugwe rejects 57 pairs MultiInsert, 17 AllomorphOutput, 21 RuleKind). The largest excluded class is same-side pairs (Sena: 84 rejections, the C(13,2) suffix pairs), and those can never commute — stem+A+B ≠ stem+B+A is arithmetic, not conservatism.
One consequence worth separating from performance. The prune is not signature-preserving and cannot be: a parse signature carries MorphemesInApplicationOrder, so unapplying a prefix before a suffix and the reverse are two distinct parses of one word even when the rules commute. What it preserves is the morph sequence and the root; what it drops is derivational history. Those two rows are indistinguishable to a reader, so this is an ambiguity reduction as much as an optimization — and it would need the correctness treatment rather than being presented as invisible.
This is now the fifth instance of the pattern in this ledger's "lesson that generalises": apparent redundancy that evaporates once the key is complete.
On rows 10 and 11 — reachability
The first reading of our data looked like it reopened the lexical reachability gate: the heavy Sena word made 272,152 unapplication attempts and the Aweti word 505,600, each finding zero roots. It does not reopen it. Those lexical-lookup counts come from runs truncated by a step budget, so they show the search never reached lookup within budget, not that lookup would have failed. Row 11's finding — that the expensive words fail on checks running after lexical lookup succeeds — is untouched by anything measured here.
On the complexity-cap step budget
b3fd2b55 was cherry-picked to bound the pathological words. It works where the cost is many rule applications: an Aweti word that previously crashed a test host at 12.1 GB after 1,717 s completes in 50 s at 200,000 steps, and whole-corpus runs go from 22/40 words to 40/40 at 757 MB peak instead of 12 GB.
It does not bound a word whose cost is one expensive traversal. An Amharic word ran 2,280 s under a 200,000-step budget with the nondeterministic counters frozen at 217,493 traversals and 53.9 M arc checks across successive samples — stuck inside a single matcher call. The budget is checked between leaf-rule applications and cannot interrupt one. Anyone relying on it as a safety bound should know that.
Also worth flagging for consumers: the budget ships on by default (2,000,000 steps, 10 s). A 10-second default silently truncates parses.
Reproduction
Branch based on master a4b2974. Measurements used a census harness that parses each word twice in one process with MaxDegreeOfParallelism: 1 and tracing on (so the analysis memo and MergeEquivalentAnalyses are off and counts are the unmemoized search), compares complete parse-signature multisets, and reports MorphologicalRuleAnalysis trace nodes as attempts and CheckInputMatch calls as arc checks. Grammars are FieldWorks GenerateHCConfig exports of five projects; word lists are the first 40 entries except Indonesian (all 121) and Sena (entries 1001–1040, since the head of that list is punctuation). Machine-safety aborts and test-host crashes are recorded as such and excluded from ratios, never counted as agreement.
|
Two observations from the same investigation that belong in the record but are not optimizations, so they did not fit the rows above. A repeatable memory wall at 12.2–12.6 GB, independent of any capAcross the five-grammar runs, 13 words (Sena and Aweti) ended as test-host crashes rather than completions, and they did so at a strikingly consistent managed heap: 12,225 / 12,259 / 12,270 / 12,278 / 12,282 / 12,312 / 12,323 / 12,324 / 12,336 / 12,340 / 12,359 / 12,365 / 12,563 MB. The consistency holds regardless of the per-process limit in force — including runs whose limit was 20 GB, so these are not the harness stopping them. The clustering at ~12.3 GB rather than at whatever ceiling was configured is the interesting part; it is the signature one would expect from a single collection outgrowing the 2 GB per-object limit rather than from address-space or RAM exhaustion. A crash dump on one of these words would name the collection. Reported as an observation only — no diagnosis attempted. For scale, one Aweti word (line 183 of its list) reached 13.2 GB and 30.1 M nondeterministic traversals / 139.3 M arc checks in 53 minutes without leaving analysis, and never reported a lexical lookup. Under a 200,000-step budget the same word completes in 231 s at 676 MB with 505,600 rule-unapplication attempts and 0 deterministic traversals. Where analysis time goes, by traversal familyThe counters added for this work split every
This is offered as calibration for anyone sizing a synthesis-side optimization: on the two grammars with the heaviest analysis, 99.8 % or more of FST work is on the nondeterministic side. It is consistent with the ledger's Amharic breakdown (analysis cascade ~95 % of analysis, all synthesis 0.3 %), measured independently and at the arc-check level rather than by wall-time bucket. |
|
Added one record from a September round that post-dates this set: Three things it contributes to this ledger:
Also closes the two extensions before anyone builds them — environment matchers are 0.20%/1.18% of traversal instances on Amharic/Mbugwe (this is #515), and the analysis rules' 93.95% is 99.7% alternate captures because
🤖 Generated with Claude Code |
2026-09-23 round: the missing Aweti speedup was reduplication, not traversalOne new row closes the gap #511 opened. Aweti index 182 had never completed here, on any stack we tried (#491, #494, #511, FieldWorks #1148, forced final templates, FLEx's parallel/no-memo mode). A copy-agreement prune in reduplication unapplication makes it finish in 9 s, and the whole Aweti list runs 4.95× faster with 0 divergences across five grammars. Proposed as #519 (on by default) and shipped in PanGloss v0.4.0. New row
Why the ledger missed it: every prior row measured Sena, Mbugwe and Amharic, where full-copy rules are absent or rare. The 19 docs mention reduplication once (the probe-design classifier, marked "Unknown"). What else this round settled
PanGloss v0.4.0 (same prune, Rust)Release binaries, same machine, v0.3.3 vs v0.4.0: identical analyses on all 7,455 words both complete across the five grammars. Aweti: 172 -> 191 words complete within 20 s, 5.3x on the 172 shared words, median word 188 -> 26 ms, word-list index 182 60 s timeout -> 2.7 s. Sena, Amharic and Mbugwe are within noise to about 10% slower; a per-call allocation on rules that copy nothing is being removed. Open since this round
Method notes worth keeping
🤖 Generated with Claude Code |
47318eb to
5bbfa01
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
| @@ -0,0 +1,439 @@ | |||
| # HermitCrab analysis-side optimization, 2026-10-27 round | |||
|
|
|||
| **Disposition: seven candidates are PRs (R1 #575, R2 #572, T1 #571, R5 #574, R3 #576, R1 on #546 #577, R1 purity extension #579), one is a | |||
Public API, target framework, package, and machine.py parity: None verified (docs only). Findings: F1-F19 addressed; F20, F21, F22, F23, F24, F25, F26, F27, F28, F29, F30, F31, F32, F33, F34, F35, F36, F37, F38, F39, F40, F41, F42, F44 open; F43 open, unverified; F45, F46 new. Reviewed at 2eba2f5 |
Start here:
docs/hermitcrab-perf-2026-09-disposition.md: why the faster September package was archived rather than merged, and what any revival must fix first.Status: open as a record; not intended to merge code. The only non-skipped check is green (the
claudejob is skipped).This is a documentation-only record of the HermitCrab optimization rounds (22 files, all under
docs/): the attempt ledger, nine retained mechanism records, five rejected-optimization records, the measurement method, and the round write-ups for September 2026 and October 2026. No parser behaviour changes, and the archived mechanisms (including the edge prefilter) stay off master.Why keep a record of work that was not merged? The September package measured faster in wall-clock terms (1.76x Amharic, 1.83x Mbugwe; secondary figures, later rounds count steps), but the figure cannot be attributed to any one mechanism: only the deferred template clone was ever timed alone, at 1.054x. Its parity gate also compared morph counts, not analyses. The record lets the next attempt reuse the evidence instead of repeating it.
Where to look:
docs/hermitcrab-perf-2026-09-counts.md("legacyWordAnalysisSignaturecheck") shows the signature compared morph counts only on the FLEx exports, becauseMorpheme.Idis empty.perf/hc-optimization-archive, which still holds the edge prefilter (git grep EdgePrefilterEnabled).docs/hermitcrab-optimization-ledger.md, one row per attempt.docs/rejected-optimizations/; the traversal-dedupe record covers Add allMatches to Traverse function to improve performance #511 (still open).Deliberately not here: source changes and re-measurement. Reviving any archived mechanism needs a complete parse-identity signature first (gloss, allomorph index, syntactic feature structure, ideally the full multiset with multiplicities), not this PR.
Related PRs: #491 (merged) is the larger win the September disposition redirected to; #546 (open) is the template-slot sharing the October search-cost round ported to C#.
Verification:
git diff --check origin/master...HEADclean; comment-hygiene found no in-scope files (docs-only diff).dotnet build,dotnet test,dotnet csharpier check .and./local_check.shwere not run: no.csor.csprojtouched.Next: confirm the archive branch
perf/hc-optimization-archiveshould stay as the home for the unmerged package, then close or merge this record.Reading this a year from now -- start here
This record covers the September 2026 optimization round, the October 2026 search-cost round, and two October 27 rounds (synthesis-to-analysis pruning, then analysis-side optimization), plus a search-asymptotics research note. "Retained" does not mean merged or production-ready: the September package was rejected for merge.
The package's branch-wide sample was faster on all five languages measured, "stable" on two of them per the probe rules, but not attributable to any mechanism. That non-attribution, not an absence of speedup, is why it was archived.
Correctness limit: the branch-wide parity gate used
MorpherTests.WordAnalysisSignature, which joinsMorpheme.Id. That is empty for every FLEx export used, so on Sena, Mbugwe and Amharic it degenerates to a morph count per analysis: it verified counts, not analyses.Decisions and why
SIL.Machine.FeatureModel.FeatureStruct, the rest in FST traversal internals,Word,Shapeand the analysis rules: not reviewable as one change.AlwaysEnforceFinalTemplates = true), gave identical analysis sets with far larger gains on Mbugwe and Sena than the package; Amharic barely moves because its per-word cost is not in template interleaving. The package is worth keeping only where Filter final templates in analysis #491 cannot fire, and only in its most reviewable piece.docs/hermitcrab-probe-design.md.Surprising findings
Paths not taken
Five rejection records under
docs/rejected-optimizations/:alternatives-memo-and-hoist.md,compounding-split-prefilter.md,final-template-prune.md,two-pass-nondeterministic-traversal.md, andstate-position-traversal-dedup.md(the traversal dedupe of #511, added after the first round closed). Lexically-dead branch pruning, ranked first indocs/hermitcrab-search-asymptotics-research.md, was measured and rejected on its ceiling. Template replay (E3) was dropped on cost.Deferred, and what would unblock it
October analysis-side candidates live as separate PRs (#571, #574, #575, #576, #577, #579 open; #572 closed) with their own records in
docs/hermitcrab-2026-10-27-analysis-round.md. Merging-while-tracing is an owner product decision, not built. Exact finite-state compilation and partial-order reduction are larger projects described only as research in the asymptotics note. Any renewed branch-wide parity claim needs the full parse-identity signature first.Evidence, scope, and provenance
Durable records:
docs/hermitcrab-optimization-ledger.md,docs/hermitcrab-perf-2026-09-disposition.md,docs/hermitcrab-perf-2026-09-counts.md,docs/hermitcrab-probe-design.md, nine mechanism records underdocs/optimizations/, five rejection records underdocs/rejected-optimizations/, and the round write-upsdocs/hermitcrab-2026-10-21-search-cost-round.md,docs/hermitcrab-2026-10-27-synthesis-to-analysis-round.md,docs/hermitcrab-2026-10-27-analysis-round.md,docs/hermitcrab-search-asymptotics-research.md.The unmerged package is archived on
perf/hc-optimization-archive. The separately isolatedperf/hc-edge-prefilterbranch and its archive tag were deleted, but the edge-prefilter mechanism (source and tests) still lives in the whole-round archive, found withgit grep EdgePrefilterEnabled.The earlier body quoted a historical NUnit result from the prior measurement; it is not carried forward because it was not rerun.
🤖 Generated with Claude Code
This change is